/* Arduino Radar Clock on CrowPanel 2.1inch-HMI ESP32 Rotary Display 480*480 by mircemk, October 2025*/ #include #include #include #include // WiFi credentials const char* ssid = "*****"; // Replace with your WiFi SSID const char* password = "*****"; // Replace with your WiFi password // NTP Server settings const char* ntpServer = "pool.ntp.org"; const long gmtOffset_sec = 3600; // Change according to your timezone (3600 = GMT+1) const int daylightOffset_sec = 3600; /* ======== DISPLAY CONFIGURATION ======== */ #define TYPE_SEL 7 // ST7701 init table #define PCLK_NEG 1 // 1 = falling edge #define TIMING_SET 1 // 1 = wider safe porches /* --- Backlight PIN --- */ #define BL_PIN 6 /* --- SPI for ST7701 init --- */ #define PANEL_CS 16 #define PANEL_SCK 2 #define PANEL_SDA 1 /* --- Rotary Encoder Pins --- */ #define ENCODER_A_PIN 42 #define ENCODER_B_PIN 44 /* --- Display Timing --- */ #if TIMING_SET == 0 static const int HFP=20, HPW=10, HBP=10; static const int VFP=8, VPW=10, VBP=10; #else static const int HFP=40, HPW=8, HBP=40; static const int VFP=20, VPW=8, VBP=20; #endif /* ======== RADAR CONFIGURATION ======== */ #define DISPLAY_WIDTH 480 #define DISPLAY_HEIGHT 480 #define CENTER_X 240 #define CENTER_Y 240 #define RADAR_RADIUS 200 #define FRAME_START 200 #define FRAME_WIDTH 5 #define BORDER_WIDTH 5 #define SWEEP_SPEED 3 /* ======== COLOR SCHEMES ======== */ // Color definitions for different schemes (RGB565) typedef struct { uint16_t green_color; uint16_t dim_green_color; uint16_t sweep_color; uint16_t grid_color; uint16_t frame_color; uint16_t text_color; uint16_t trail_color; uint16_t black; } ColorScheme; // Scheme 1: Classic Green (original) const ColorScheme SCHEME_GREEN = { 0x07E0, // green_color 0x03A0, // dim_green_color 0x07FF, // sweep_color (cyan) 0x03E0, // grid_color 0x07E0, // frame_color 0x07E0, // text_color 0x0320, // trail_color 0x0000 // black }; // Scheme 2: Red const ColorScheme SCHEME_RED = { 0xF800, // green_color -> red 0x7800, // dim_green_color -> dark red 0xF810, // sweep_color -> bright red 0xF800, // grid_color -> red 0xF800, // frame_color -> red 0xF800, // text_color -> red 0x7800, // trail_color -> dark red 0x0000 // black }; // Scheme 3: Blue (like second vector link - digital radar) const ColorScheme SCHEME_BLUE = { 0x001F, // green_color -> blue 0x0010, // dim_green_color -> dark blue 0x041F, // sweep_color -> bright blue 0x001F, // grid_color -> blue 0x001F, // frame_color -> blue 0x001F, // text_color -> blue 0x0010, // trail_color -> dark blue 0x0000 // black }; // Scheme 4: Yellow-Orange const ColorScheme SCHEME_YELLOW_ORANGE = { 0xFDA0, // green_color -> yellow-orange 0xFB00, // dim_green_color -> dark yellow-orange 0xFEA0, // sweep_color -> bright yellow-orange 0xFDA0, // grid_color -> yellow-orange 0xFDA0, // frame_color -> yellow-orange 0xFDA0, // text_color -> yellow-orange 0xFB00, // trail_color -> dark yellow-orange 0x0000 // black }; // Scheme 5: White const ColorScheme SCHEME_WHITE = { 0xFFFF, // green_color -> white 0xDEFB, // dim_green_color -> light gray 0xFFFF, // sweep_color -> white 0xFFFF, // grid_color -> white 0xFFFF, // frame_color -> white 0xFFFF, // text_color -> white 0xBDF7, // trail_color -> medium gray 0x0000 // black }; const ColorScheme* colorSchemes[] = { &SCHEME_GREEN, &SCHEME_RED, &SCHEME_BLUE, &SCHEME_YELLOW_ORANGE, &SCHEME_WHITE }; #define NUM_COLOR_SCHEMES 5 /* ======== GLOBAL VARIABLES ======== */ Arduino_DataBus *panelBus = nullptr; Arduino_ESP32RGBPanel *rgbpanel = nullptr; Arduino_RGB_Display *gfx = nullptr; static float current_angle = 0; static uint16_t *frameBuffer = nullptr; static uint16_t *staticFrameBuffer = nullptr; // Color scheme management int currentColorScheme = 0; bool colorSchemeChanged = true; // Rotary encoder variables volatile int encoderPos = 0; int lastEncoderPos = 0; portMUX_TYPE encoderMux = portMUX_INITIALIZER_UNLOCKED; // Startup sequence state enum StartupState { SHOW_TITLE, SHOW_CONNECTING, SHOW_CLOCK }; StartupState currentStartupState = SHOW_TITLE; unsigned long startupStartTime = 0; // WiFi connection state bool wifiConnecting = false; bool wifiConnected = false; unsigned long lastWiFiCheck = 0; const unsigned long WIFI_CHECK_INTERVAL = 1000; // Display stability bool displayStable = false; // Encoder interrupt service routine void IRAM_ATTR encoderISR() { portENTER_CRITICAL_ISR(&encoderMux); static uint8_t old_AB = 0; // Grey code // 0b00: 0 // 0b01: 1 // 0b11: 2 // 0b10: 3 const int8_t enc_states[] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0}; old_AB <<= 2; // Remember previous state old_AB |= (digitalRead(ENCODER_A_PIN) ? (1 << 1) : 0) | (digitalRead(ENCODER_B_PIN) ? (1 << 0) : 0); encoderPos += enc_states[(old_AB & 0x0f)]; portEXIT_CRITICAL_ISR(&encoderMux); } void handleEncoder() { portENTER_CRITICAL(&encoderMux); int currentPos = encoderPos; portEXIT_CRITICAL(&encoderMux); if (currentPos != lastEncoderPos && currentStartupState == SHOW_CLOCK) { if (currentPos > lastEncoderPos) { // Clockwise rotation - next color scheme currentColorScheme = (currentColorScheme + 1) % NUM_COLOR_SCHEMES; } else { // Counter-clockwise rotation - previous color scheme currentColorScheme = (currentColorScheme - 1 + NUM_COLOR_SCHEMES) % NUM_COLOR_SCHEMES; } colorSchemeChanged = true; Serial.printf("Color scheme changed to: %d\n", currentColorScheme + 1); lastEncoderPos = currentPos; } } /* ======== STARTUP SCREEN FUNCTIONS ======== */ void showTitleScreen() { // Use direct display drawing for maximum stability gfx->fillScreen(SCHEME_GREEN.black); // Draw "RADAR CLOCK" - larger text gfx->setTextColor(SCHEME_GREEN.text_color); gfx->setTextSize(4); // Calculate position for "RADAR CLOCK" String radarText = "RADAR CLOCK"; int16_t x1, y1; uint16_t w, h; gfx->getTextBounds(radarText, 0, 0, &x1, &y1, &w, &h); int radarX = (DISPLAY_WIDTH - w) / 2; int radarY = CENTER_Y - 60; gfx->setCursor(radarX, radarY); gfx->print(radarText); // Draw "by" - smaller text gfx->setTextSize(2); String byText = "by"; gfx->getTextBounds(byText, 0, 0, &x1, &y1, &w, &h); int byX = (DISPLAY_WIDTH - w) / 2; int byY = CENTER_Y; gfx->setCursor(byX, byY); gfx->print(byText); // Draw "Mircemk" - medium text gfx->setTextSize(3); String nameText = "Mircemk"; gfx->getTextBounds(nameText, 0, 0, &x1, &y1, &w, &h); int nameX = (DISPLAY_WIDTH - w) / 2; int nameY = CENTER_Y + 40; gfx->setCursor(nameX, nameY); gfx->print(nameText); } void showConnectingScreen() { // Use direct display drawing for stability during WiFi connection gfx->fillScreen(SCHEME_GREEN.black); // Draw "connecting..." text gfx->setTextColor(SCHEME_GREEN.text_color); gfx->setTextSize(3); String connectingText = "connecting..."; int16_t x1, y1; uint16_t w, h; gfx->getTextBounds(connectingText, 0, 0, &x1, &y1, &w, &h); int textX = (DISPLAY_WIDTH - w) / 2; int textY = CENTER_Y; gfx->setCursor(textX, textY); gfx->print(connectingText); // Draw the display immediately using the frame buffer method for stability if (frameBuffer) { gfx->draw16bitRGBBitmap(0, 0, frameBuffer, DISPLAY_WIDTH, DISPLAY_HEIGHT); } } void updateConnectingAnimation() { static unsigned long lastDotUpdate = 0; static bool dotVisible = false; unsigned long currentTime = millis(); if (currentTime - lastDotUpdate >= 500) { // Blink every 500ms lastDotUpdate = currentTime; dotVisible = !dotVisible; // Update dot without redrawing entire screen if (dotVisible) { gfx->fillRect(DISPLAY_WIDTH - 30, CENTER_Y + 40, 10, 10, SCHEME_GREEN.text_color); } else { gfx->fillRect(DISPLAY_WIDTH - 30, CENTER_Y + 40, 10, 10, SCHEME_GREEN.black); } // Update only the changed area gfx->draw16bitRGBBitmap(DISPLAY_WIDTH - 30, CENTER_Y + 40, &frameBuffer[CENTER_Y * DISPLAY_WIDTH + (DISPLAY_WIDTH - 30)], 10, 10); } } bool connectToWiFi() { Serial.print("Connecting to WiFi"); WiFi.begin(ssid, password); unsigned long startTime = millis(); const unsigned long timeout = 30000; // 30 seconds timeout while (WiFi.status() != WL_CONNECTED && millis() - startTime < timeout) { delay(500); Serial.print("."); // Update connecting animation updateConnectingAnimation(); } Serial.println(); if (WiFi.status() == WL_CONNECTED) { Serial.println("WiFi connected!"); Serial.print("IP address: "); Serial.println(WiFi.localIP()); return true; } else { Serial.println("WiFi connection failed!"); return false; } } void updateStartupSequence() { unsigned long currentTime = millis(); unsigned long elapsedTime = currentTime - startupStartTime; switch(currentStartupState) { case SHOW_TITLE: if (elapsedTime >= 2000) { // Show title for 2 seconds currentStartupState = SHOW_CONNECTING; startupStartTime = currentTime; showConnectingScreen(); Serial.println("Showing connecting screen..."); // Start WiFi connection wifiConnecting = true; WiFi.begin(ssid, password); } break; case SHOW_CONNECTING: // Update connecting animation updateConnectingAnimation(); // Check WiFi status periodically if (currentTime - lastWiFiCheck >= WIFI_CHECK_INTERVAL) { lastWiFiCheck = currentTime; if (WiFi.status() == WL_CONNECTED) { wifiConnecting = false; wifiConnected = true; currentStartupState = SHOW_CLOCK; startupStartTime = currentTime; Serial.println("WiFi connected! Showing clock..."); // Initialize the clock display initClockDisplay(); displayStable = true; } } break; case SHOW_CLOCK: // Clock is now running in main loop break; } } /* ======== DRAWING FUNCTIONS ======== */ void draw_line_to_buffer(uint16_t *buffer, int x0, int y0, int x1, int y1, uint16_t color) { int dx = abs(x1 - x0); int dy = abs(y1 - y0); int sx = (x0 < x1) ? 1 : -1; int sy = (y0 < y1) ? 1 : -1; int err = dx - dy; while (true) { if (x0 >= 0 && x0 < DISPLAY_WIDTH && y0 >= 0 && y0 < DISPLAY_HEIGHT) { buffer[y0 * DISPLAY_WIDTH + x0] = color; } if (x0 == x1 && y0 == y1) break; int e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } } void draw_rectangle_to_buffer(uint16_t *buffer, int x1, int y1, int x2, int y2, uint16_t color) { for (int y = y1; y <= y2; y++) { for (int x = x1; x <= x2; x++) { if (x >= 0 && x < DISPLAY_WIDTH && y >= 0 && y < DISPLAY_HEIGHT) { buffer[y * DISPLAY_WIDTH + x] = color; } } } } void draw_digit_to_buffer(uint16_t *buffer, int x, int y, int digit, uint16_t color) { // Increased sizes (3x original) int width = 18; // Was 6 int height = 24; // Was 8 // Clear background for larger digit draw_rectangle_to_buffer(buffer, x-9, y-12, x+9, y+12, colorSchemes[currentColorScheme]->black); switch(digit) { case 0: draw_line_to_buffer(buffer, x-6, y-9, x+6, y-9, color); // top draw_line_to_buffer(buffer, x-6, y+9, x+6, y+9, color); // bottom draw_line_to_buffer(buffer, x-6, y-9, x-6, y+9, color); // left draw_line_to_buffer(buffer, x+6, y-9, x+6, y+9, color); // right break; case 1: draw_line_to_buffer(buffer, x, y-9, x, y+9, color); // vertical draw_line_to_buffer(buffer, x-3, y-9, x, y-9, color); // top break; case 2: draw_line_to_buffer(buffer, x-6, y-9, x+6, y-9, color); // top draw_line_to_buffer(buffer, x+6, y-9, x+6, y, color); // right top draw_line_to_buffer(buffer, x-6, y, x+6, y, color); // middle draw_line_to_buffer(buffer, x-6, y, x-6, y+9, color); // left bottom draw_line_to_buffer(buffer, x-6, y+9, x+6, y+9, color); // bottom break; case 3: draw_line_to_buffer(buffer, x-6, y-9, x+6, y-9, color); // top draw_line_to_buffer(buffer, x+6, y-9, x+6, y+9, color); // right draw_line_to_buffer(buffer, x-6, y, x+6, y, color); // middle draw_line_to_buffer(buffer, x-6, y+9, x+6, y+9, color); // bottom break; case 4: draw_line_to_buffer(buffer, x-6, y-9, x-6, y, color); // left top draw_line_to_buffer(buffer, x-6, y, x+6, y, color); // middle draw_line_to_buffer(buffer, x+6, y-9, x+6, y+9, color); // right break; case 5: draw_line_to_buffer(buffer, x-6, y-9, x+6, y-9, color); // top draw_line_to_buffer(buffer, x-6, y-9, x-6, y, color); // left top draw_line_to_buffer(buffer, x-6, y, x+6, y, color); // middle draw_line_to_buffer(buffer, x+6, y, x+6, y+9, color); // right bottom draw_line_to_buffer(buffer, x-6, y+9, x+6, y+9, color); // bottom break; case 6: draw_line_to_buffer(buffer, x-6, y-9, x+6, y-9, color); // top draw_line_to_buffer(buffer, x-6, y-9, x-6, y+9, color); // left draw_line_to_buffer(buffer, x-6, y, x+6, y, color); // middle draw_line_to_buffer(buffer, x+6, y, x+6, y+9, color); // right bottom draw_line_to_buffer(buffer, x-6, y+9, x+6, y+9, color); // bottom break; case 7: draw_line_to_buffer(buffer, x-6, y-9, x+6, y-9, color); // top draw_line_to_buffer(buffer, x+6, y-9, x+6, y+9, color); // right break; case 8: draw_line_to_buffer(buffer, x-6, y-9, x+6, y-9, color); // top draw_line_to_buffer(buffer, x-6, y-9, x-6, y+9, color); // left draw_line_to_buffer(buffer, x+6, y-9, x+6, y+9, color); // right draw_line_to_buffer(buffer, x-6, y, x+6, y, color); // middle draw_line_to_buffer(buffer, x-6, y+9, x+6, y+9, color); // bottom break; case 9: draw_line_to_buffer(buffer, x-6, y-9, x+6, y-9, color); // top draw_line_to_buffer(buffer, x-6, y-9, x-6, y, color); // left top draw_line_to_buffer(buffer, x+6, y-9, x+6, y+9, color); // right draw_line_to_buffer(buffer, x-6, y, x+6, y, color); // middle draw_line_to_buffer(buffer, x-6, y+9, x+6, y+9, color); // bottom break; } } void draw_text_to_buffer(uint16_t *buffer, int x, int y, const char* text, uint16_t color) { int char_width = 24; // Increased from 8 to 24 for larger spacing int pos_x = x; while (*text) { char c = *text++; if (c >= '0' && c <= '9') { draw_digit_to_buffer(buffer, pos_x, y, c - '0', color); } else if (c == ':') { // Draw larger colon buffer[(y - 6) * DISPLAY_WIDTH + pos_x] = color; buffer[(y - 5) * DISPLAY_WIDTH + pos_x] = color; buffer[(y + 5) * DISPLAY_WIDTH + pos_x] = color; buffer[(y + 6) * DISPLAY_WIDTH + pos_x] = color; } else if (c == '/') { // Draw larger slash for (int i = -9; i <= 9; i++) { int px = pos_x + i; int py = y - i; if (px >= 0 && px < DISPLAY_WIDTH && py >= 0 && py < DISPLAY_HEIGHT) { buffer[py * DISPLAY_WIDTH + px] = color; } } } pos_x += char_width; } } void update_time_display() { struct tm timeinfo; if(!getLocalTime(&timeinfo)) { Serial.println("Failed to obtain time"); return; } // Calculate positions // Font height is 24, so three heights = 72 pixels int time_y = CENTER_Y - 72; // Move up by three font heights int date_y = CENTER_Y + 72; // Move down by three font heights int time_x = CENTER_X - 90; // Keep the same horizontal position int date_x = CENTER_X - 105; // Keep the same horizontal position // Clear previous time area - precise clearing // Height of clearing = font height (24) + 2 pixels margin // Width of clearing = 8 digits * 24 pixels width + 4 pixels margin for (int y = time_y - 13; y < time_y + 13; y++) { for (int x = time_x - 2; x < time_x + (8 * 24) + 2; x++) { if (x >= 0 && x < DISPLAY_WIDTH && y >= 0 && y < DISPLAY_HEIGHT) { frameBuffer[y * DISPLAY_WIDTH + x] = colorSchemes[currentColorScheme]->black; } } } // Format time string char timeStr[9]; sprintf(timeStr, "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); // Draw time draw_text_to_buffer(frameBuffer, time_x, time_y, timeStr, colorSchemes[currentColorScheme]->text_color); // Clear previous date area - precise clearing // Height of clearing = font height (24) + 2 pixels margin // Width of clearing = 10 digits * 24 pixels width + 4 pixels margin for (int y = date_y - 13; y < date_y + 13; y++) { for (int x = date_x - 2; x < date_x + (10 * 24) + 2; x++) { if (x >= 0 && x < DISPLAY_WIDTH && y >= 0 && y < DISPLAY_HEIGHT) { frameBuffer[y * DISPLAY_WIDTH + x] = colorSchemes[currentColorScheme]->black; } } } // Format date string char dateStr[11]; sprintf(dateStr, "%02d/%02d/%04d", timeinfo.tm_mday, timeinfo.tm_mon + 1, timeinfo.tm_year + 1900); // Draw date draw_text_to_buffer(frameBuffer, date_x, date_y, dateStr, colorSchemes[currentColorScheme]->text_color); } void redrawStaticElements() { // Clear static frame buffer for (int i = 0; i < DISPLAY_WIDTH * DISPLAY_HEIGHT; i++) { staticFrameBuffer[i] = colorSchemes[currentColorScheme]->black; } // Redraw all static elements with new colors draw_frame_border(); draw_radar_grid(); // Copy to main frame buffer for (int i = 0; i < DISPLAY_WIDTH * DISPLAY_HEIGHT; i++) { frameBuffer[i] = staticFrameBuffer[i]; } colorSchemeChanged = false; Serial.println("Static elements redrawn with new color scheme"); } /* ======== DISPLAY INITIALIZATION ======== */ void init_display() { Serial.println("Initializing display..."); pinMode(BL_PIN, OUTPUT); digitalWrite(BL_PIN, HIGH); delay(100); // Initialize rotary encoder pins pinMode(ENCODER_A_PIN, INPUT_PULLUP); pinMode(ENCODER_B_PIN, INPUT_PULLUP); // Attach interrupts for rotary encoder attachInterrupt(digitalPinToInterrupt(ENCODER_A_PIN), encoderISR, CHANGE); attachInterrupt(digitalPinToInterrupt(ENCODER_B_PIN), encoderISR, CHANGE); panelBus = new Arduino_SWSPI( GFX_NOT_DEFINED, PANEL_CS, PANEL_SCK, PANEL_SDA, GFX_NOT_DEFINED ); rgbpanel = new Arduino_ESP32RGBPanel( 40, 7, 15, 41, 46, 3, 8, 18, 17, 14, 13, 12, 11, 10, 9, 5, 45, 48, 47, 21, 1, 50, 10, 50, 1, 30, 10, 30, PCLK_NEG, 8000000UL ); #if TYPE_SEL == 7 gfx = new Arduino_RGB_Display( DISPLAY_WIDTH, DISPLAY_HEIGHT, rgbpanel, 0, true, panelBus, GFX_NOT_DEFINED, st7701_type7_init_operations, sizeof(st7701_type7_init_operations) ); #else gfx = new Arduino_RGB_Display( DISPLAY_WIDTH, DISPLAY_HEIGHT, rgbpanel, 0, true, panelBus, GFX_NOT_DEFINED, st7701_type5_init_operations, sizeof(st7701_type5_init_operations) ); #endif Serial.println("Starting display begin..."); bool ok = gfx->begin(16000000); Serial.printf("Display begin: %s\n", ok ? "OK" : "FAILED"); if (!ok) { Serial.println("Display initialization failed!"); while(1) delay(1000); } // Allocate main frame buffer with extra margin for safety frameBuffer = (uint16_t*)ps_malloc(DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t) + 32); if (!frameBuffer) { Serial.println("Frame buffer allocation failed!"); while(1) delay(1000); } // Allocate static frame buffer for unchanging elements with extra margin staticFrameBuffer = (uint16_t*)ps_malloc(DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t) + 32); if (!staticFrameBuffer) { Serial.println("Static frame buffer allocation failed!"); while(1) delay(1000); } // Clear both buffers to black for (int i = 0; i < DISPLAY_WIDTH * DISPLAY_HEIGHT; i++) { frameBuffer[i] = colorSchemes[currentColorScheme]->black; staticFrameBuffer[i] = colorSchemes[currentColorScheme]->black; } Serial.println("Display initialized successfully"); } void initClockDisplay() { // Draw static elements to static buffer draw_frame_border(); draw_radar_grid(); // Copy static elements to main frame buffer for (int i = 0; i < DISPLAY_WIDTH * DISPLAY_HEIGHT; i++) { frameBuffer[i] = staticFrameBuffer[i]; } // Init and get the time configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); Serial.println("Clock display initialized"); } void draw_circle_to_buffer(uint16_t *buffer, int center_x, int center_y, int radius, uint16_t color) { int x = radius; int y = 0; int err = 0; while (x >= y) { buffer[(center_y + y) * DISPLAY_WIDTH + (center_x + x)] = color; buffer[(center_y + x) * DISPLAY_WIDTH + (center_x + y)] = color; buffer[(center_y + x) * DISPLAY_WIDTH + (center_x - y)] = color; buffer[(center_y + y) * DISPLAY_WIDTH + (center_x - x)] = color; buffer[(center_y - y) * DISPLAY_WIDTH + (center_x + x)] = color; buffer[(center_y - x) * DISPLAY_WIDTH + (center_x + y)] = color; buffer[(center_y - x) * DISPLAY_WIDTH + (center_x - y)] = color; buffer[(center_y - y) * DISPLAY_WIDTH + (center_x - x)] = color; y++; err += 1 + 2 * y; if (2 * (err - x) + 1 > 0) { x--; err += 1 - 2 * x; } } } void draw_arc_to_buffer(uint16_t *buffer, int center_x, int center_y, int radius, int start_angle, int end_angle, uint16_t color) { // Convert angles to radians float start_rad = start_angle * PI / 180.0; float end_rad = end_angle * PI / 180.0; // Draw arc by stepping through angles for (float angle = start_rad; angle <= end_rad; angle += 0.01) { int x = center_x + radius * cos(angle); int y = center_y + radius * sin(angle); if (x >= 0 && x < DISPLAY_WIDTH && y >= 0 && y < DISPLAY_HEIGHT) { buffer[y * DISPLAY_WIDTH + x] = color; } } } void draw_radial_scale_marks() { for (int angle = 0; angle < 360; angle += 10) { if ((angle >= 355 || angle <= 5) || (angle >= 85 && angle <= 95) || (angle >= 175 && angle <= 185) || (angle >= 265 && angle <= 275)) { continue; } float rad = angle * PI / 180.0; int inner_x = CENTER_X + (FRAME_START + FRAME_WIDTH) * sin(rad); int inner_y = CENTER_Y - (FRAME_START + FRAME_WIDTH) * cos(rad); int outer_x = CENTER_X + (FRAME_START + FRAME_WIDTH + 12) * sin(rad); int outer_y = CENTER_Y - (FRAME_START + FRAME_WIDTH + 12) * cos(rad); draw_line_to_buffer(staticFrameBuffer, inner_x, inner_y, outer_x, outer_y, colorSchemes[currentColorScheme]->frame_color); } for (int angle = 0; angle < 360; angle += 30) { if (angle % 90 == 0) continue; if ((angle >= 355 || angle <= 5) || (angle >= 85 && angle <= 95) || (angle >= 175 && angle <= 185) || (angle >= 265 && angle <= 275)) { continue; } float rad = angle * PI / 180.0; int inner_x = CENTER_X + (FRAME_START + FRAME_WIDTH) * sin(rad); int inner_y = CENTER_Y - (FRAME_START + FRAME_WIDTH) * cos(rad); int outer_x = CENTER_X + (FRAME_START + FRAME_WIDTH + 20) * sin(rad); int outer_y = CENTER_Y - (FRAME_START + FRAME_WIDTH + 20) * cos(rad); draw_line_to_buffer(staticFrameBuffer, inner_x, inner_y, outer_x, outer_y, colorSchemes[currentColorScheme]->frame_color); } } void draw_cardinal_directions() { const int gap_size = 10; // North int north_x = CENTER_X; int north_y = CENTER_Y - FRAME_START - FRAME_WIDTH - 20; for (int dx = -12; dx <= 12; dx++) { for (int dy = -12; dy <= 12; dy++) { int px = north_x + dx; int py = north_y + dy; if (px >= 0 && px < DISPLAY_WIDTH && py >= 0 && py < DISPLAY_HEIGHT) { staticFrameBuffer[py * DISPLAY_WIDTH + px] = colorSchemes[currentColorScheme]->black; } } } draw_line_to_buffer(staticFrameBuffer, north_x - 8, north_y + 8, north_x - 8, north_y - 8, colorSchemes[currentColorScheme]->text_color); draw_line_to_buffer(staticFrameBuffer, north_x + 8, north_y + 8, north_x + 8, north_y - 8, colorSchemes[currentColorScheme]->text_color); draw_line_to_buffer(staticFrameBuffer, north_x - 8, north_y - 8, north_x + 8, north_y + 8, colorSchemes[currentColorScheme]->text_color); // South int south_x = CENTER_X; int south_y = CENTER_Y + FRAME_START + FRAME_WIDTH + 20; for (int dx = -12; dx <= 12; dx++) { for (int dy = -12; dy <= 12; dy++) { int px = south_x + dx; int py = south_y + dy; if (px >= 0 && px < DISPLAY_WIDTH && py >= 0 && py < DISPLAY_HEIGHT) { staticFrameBuffer[py * DISPLAY_WIDTH + px] = colorSchemes[currentColorScheme]->black; } } } draw_line_to_buffer(staticFrameBuffer, south_x - 8, south_y - 8, south_x + 8, south_y - 8, colorSchemes[currentColorScheme]->text_color); draw_line_to_buffer(staticFrameBuffer, south_x - 8, south_y - 8, south_x - 8, south_y, colorSchemes[currentColorScheme]->text_color); draw_line_to_buffer(staticFrameBuffer, south_x - 8, south_y, south_x + 8, south_y, colorSchemes[currentColorScheme]->text_color); draw_line_to_buffer(staticFrameBuffer, south_x + 8, south_y, south_x + 8, south_y + 8, colorSchemes[currentColorScheme]->text_color); draw_line_to_buffer(staticFrameBuffer, south_x - 8, south_y + 8, south_x + 8, south_y + 8, colorSchemes[currentColorScheme]->text_color); // East int east_x = CENTER_X + FRAME_START + FRAME_WIDTH + 20; int east_y = CENTER_Y; for (int dx = -12; dx <= 12; dx++) { for (int dy = -12; dy <= 12; dy++) { int px = east_x + dx; int py = east_y + dy; if (px >= 0 && px < DISPLAY_WIDTH && py >= 0 && py < DISPLAY_HEIGHT) { staticFrameBuffer[py * DISPLAY_WIDTH + px] = colorSchemes[currentColorScheme]->black; } } } draw_line_to_buffer(staticFrameBuffer, east_x - 8, east_y - 8, east_x + 8, east_y - 8, colorSchemes[currentColorScheme]->text_color); draw_line_to_buffer(staticFrameBuffer, east_x - 8, east_y, east_x + 8, east_y, colorSchemes[currentColorScheme]->text_color); draw_line_to_buffer(staticFrameBuffer, east_x - 8, east_y + 8, east_x + 8, east_y + 8, colorSchemes[currentColorScheme]->text_color); draw_line_to_buffer(staticFrameBuffer, east_x - 8, east_y - 8, east_x - 8, east_y + 8, colorSchemes[currentColorScheme]->text_color); // West int west_x = CENTER_X - FRAME_START - FRAME_WIDTH - 20; int west_y = CENTER_Y; for (int dx = -12; dx <= 12; dx++) { for (int dy = -12; dy <= 12; dy++) { int px = west_x + dx; int py = west_y + dy; if (px >= 0 && px < DISPLAY_WIDTH && py >= 0 && py < DISPLAY_HEIGHT) { staticFrameBuffer[py * DISPLAY_WIDTH + px] = colorSchemes[currentColorScheme]->black; } } } draw_line_to_buffer(staticFrameBuffer, west_x - 8, west_y - 8, west_x - 4, west_y + 8, colorSchemes[currentColorScheme]->text_color); draw_line_to_buffer(staticFrameBuffer, west_x - 4, west_y + 8, west_x, west_y - 4, colorSchemes[currentColorScheme]->text_color); draw_line_to_buffer(staticFrameBuffer, west_x, west_y - 4, west_x + 4, west_y + 8, colorSchemes[currentColorScheme]->text_color); draw_line_to_buffer(staticFrameBuffer, west_x + 4, west_y + 8, west_x + 8, west_y - 8, colorSchemes[currentColorScheme]->text_color); } void draw_frame_border() { const int gap_size = 10; for (int r = FRAME_START; r < FRAME_START + FRAME_WIDTH; r++) { draw_arc_to_buffer(staticFrameBuffer, CENTER_X, CENTER_Y, r, gap_size, 90 - gap_size, colorSchemes[currentColorScheme]->frame_color); draw_arc_to_buffer(staticFrameBuffer, CENTER_X, CENTER_Y, r, 90 + gap_size, 180 - gap_size, colorSchemes[currentColorScheme]->frame_color); draw_arc_to_buffer(staticFrameBuffer, CENTER_X, CENTER_Y, r, 180 + gap_size, 270 - gap_size, colorSchemes[currentColorScheme]->frame_color); draw_arc_to_buffer(staticFrameBuffer, CENTER_X, CENTER_Y, r, 270 + gap_size, 360 - gap_size, colorSchemes[currentColorScheme]->frame_color); } draw_radial_scale_marks(); draw_cardinal_directions(); } void draw_radar_grid() { for (int r = 50; r <= RADAR_RADIUS; r += 50) { draw_circle_to_buffer(staticFrameBuffer, CENTER_X, CENTER_Y, r, colorSchemes[currentColorScheme]->grid_color); } draw_line_to_buffer(staticFrameBuffer, CENTER_X - RADAR_RADIUS, CENTER_Y, CENTER_X + RADAR_RADIUS, CENTER_Y, colorSchemes[currentColorScheme]->grid_color); draw_line_to_buffer(staticFrameBuffer, CENTER_X, CENTER_Y - RADAR_RADIUS, CENTER_X, CENTER_Y + RADAR_RADIUS, colorSchemes[currentColorScheme]->grid_color); } void draw_radar_sweep() { float rad = current_angle * PI / 180.0; int end_x = CENTER_X + RADAR_RADIUS * sin(rad); int end_y = CENTER_Y - RADAR_RADIUS * cos(rad); for (int r = 0; r <= RADAR_RADIUS; r++) { int trail_x = CENTER_X + r * sin(rad); int trail_y = CENTER_Y - r * cos(rad); int dx = trail_x - CENTER_X; int dy = trail_y - CENTER_Y; if (dx * dx + dy * dy <= RADAR_RADIUS * RADAR_RADIUS) { uint16_t trail_color = colorSchemes[currentColorScheme]->trail_color; if (r == RADAR_RADIUS) { frameBuffer[trail_y * DISPLAY_WIDTH + trail_x] = colorSchemes[currentColorScheme]->sweep_color; } else { frameBuffer[trail_y * DISPLAY_WIDTH + trail_x] = trail_color; } } } current_angle += SWEEP_SPEED; if (current_angle >= 360) { current_angle = 0; for (int y = 0; y < DISPLAY_HEIGHT; y++) { for (int x = 0; x < DISPLAY_WIDTH; x++) { int dx = x - CENTER_X; int dy = y - CENTER_Y; if (dx * dx + dy * dy <= RADAR_RADIUS * RADAR_RADIUS) { frameBuffer[y * DISPLAY_WIDTH + x] = colorSchemes[currentColorScheme]->black; } } } for (int y = 0; y < DISPLAY_HEIGHT; y++) { for (int x = 0; x < DISPLAY_WIDTH; x++) { int dx = x - CENTER_X; int dy = y - CENTER_Y; if (dx * dx + dy * dy <= RADAR_RADIUS * RADAR_RADIUS) { frameBuffer[y * DISPLAY_WIDTH + x] = staticFrameBuffer[y * DISPLAY_WIDTH + x]; } } } } } void setup() { Serial.begin(115200); Serial.println("\n\nStarting ESP32S3 Radar Clock"); // Increase stability by setting WiFi to static mode WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(true); WiFi.persistent(true); // Initialize display init_display(); // Show title screen startupStartTime = millis(); showTitleScreen(); Serial.println("Showing title screen..."); } void loop() { // Update startup sequence updateStartupSequence(); // Only run clock functions when in clock mode if (currentStartupState == SHOW_CLOCK) { // Check for encoder rotation handleEncoder(); // Redraw static elements if color scheme changed if (colorSchemeChanged) { redrawStaticElements(); } // Update radar sweep draw_radar_sweep(); // Update time display update_time_display(); // Update display gfx->draw16bitRGBBitmap(0, 0, frameBuffer, DISPLAY_WIDTH, DISPLAY_HEIGHT); } delay(50); // ~20 FPS }